import * as path from 'path'
import { downloadFile } from '@main/utils/donwload'
import * as futils from '@main/utils/file-utils'
import * as npm from '@main/cmd/npm'
import * as cfg from '@main/truck/config'
import plimit from 'p-limit'
import { uploadOptions } from '@/common/default'

const getTarballDir = (urlText) => {
  const urlPath = new URL(urlText).pathname
  return urlPath.split('/-/')[0].substring(1)
}

const replaceMirrorUrl = (tarballUrl, mirrorUrl) => {
  const url = new URL(tarballUrl)
  return tarballUrl.replace(url.origin, mirrorUrl)
}

function getTarballName(url) {
  return url.substring(url.lastIndexOf('/') + 1)
}

const getTarballInfoByPath = (repositoryDir, tgzPath) => {
  const substr = tgzPath.replaceAll('\\', '/').replace(repositoryDir.replaceAll('\\', '/'), '')
  const index = substr.lastIndexOf('/')
  const tarballName = substr.substring(1, index)
  const tgzFilename = substr.substring(index + 1)
  const splitArray = tgzFilename.replace(path.extname(tgzFilename), '').split('-')
  const tarballVersion = splitArray[splitArray.length - 1]
  return {
    tarballName,
    tarballVersion
  }
}

const unpackingResultCallback = (result, progress, content, logCallback, progressCallback) => {
  const { isSuccess, logText, duration } = result
  progress.doneCount++
  progressCallback(progress)
  if (isSuccess) {
    logCallback(
      `[upload success] (${progress.doneCount}/${progress.totalCount}) ${content} ${duration}ms \n`
    )
  } else {
    logCallback(
      `[upload error] (${progress.doneCount}/${progress.totalCount}) ${content} \n ${logText} \n`
    )
  }
}

/**
 * 从packages-lock.json下载
 * @param {{packageLockPath:string, repositoryDir:string}} args
 * @param {*} logCallback
 * @param {*} progressCallback
 * @returns
 */
function downloadFromPackageLock(args, logCallback, progressCallback, options) {
  const { packageLockPath, repositoryDir, mirrorUrl } = args
  const { retry, concurrency } = { ...uploadOptions, ...options }

  logCallback('搜索包中...\n')
  const tarballs = []
  const packageLock = require(packageLockPath)
  enumerateDependencies(tarballs, packageLock.packages ?? packageLock.dependencies)
  logCallback(`搜索完毕, 共计${tarballs.length}\n`)

  const progress = { totalCount: tarballs.length, doneCount: 0 }
  progressCallback(progress)

  const pLimit = plimit(concurrency)

  const downloadPromises = tarballs.map((tarball) => {
    let retryNum = 0
    const downloadPackage = async (packageInfo) => {
      retryNum++
      const { tarballName, tarballUrl, tarballDir } = packageInfo
      const saveDir = path.join(repositoryDir, tarballDir, tarballName)
      await futils.mkdir(path.join(repositoryDir, tarballDir))

      const newTarbaUrl = replaceMirrorUrl(tarballUrl, mirrorUrl)
      const { isSuccess, logText, duration } = await downloadFile(newTarbaUrl, saveDir)

      if (isSuccess) {
        progress.doneCount++
        logCallback(
          `[download success] (${progress.doneCount}/${progress.totalCount}) ${newTarbaUrl} ${duration}ms [${retryNum}]\n`
        )
      } else {
        if (retryNum < retry) {
          return downloadPackage(packageInfo)
        }
        progress.doneCount++
        logCallback(
          `[download error] (${progress.doneCount}/${progress.totalCount}) ${newTarbaUrl} [${retryNum}] \n ${logText}\n`
        )
      }
      progressCallback(progress)
      return { isSuccess, logText, packageInfo }
    }

    return pLimit(async () => {
      return downloadPackage(tarball)
    })
  })

  logCallback(`准备下载  重试次数: ${retry}; 并发数: ${concurrency}\n`)
  return Promise.all(downloadPromises)
    .then((values) => {
      const totalCount = progress.totalCount
      const errorList = values.filter((item) => !item.isSuccess)
      return {
        totalCount,
        errorCount: errorList.length,
        errorList
      }
    })
    .catch((err) => {
      logCallback(err)
    })
}

function enumerateDependencies(tarballs, dependencies) {
  for (const [_, dependency] of Object.entries(dependencies)) {
    if (dependency.resolved) {
      const tarballUrl = dependency.resolved
      const tarballName = getTarballName(tarballUrl)
      const tarballDir = getTarballDir(tarballUrl)
      const tarbalVersion = dependency.version

      tarballs.push({ tarballName, tarballUrl, tarballDir, tarbalVersion })
    }
    if (dependency.dependencies) {
      enumerateDependencies(tarballs, dependency.dependencies)
    }
  }
}

const recursionfindTarballs = async (parentPath, tarballs) => {
  const subFiles = await futils.findSubFiles(parentPath)
  for (const subFile of subFiles) {
    const subFilePath = path.join(parentPath, subFile)
    const isDir = await futils.isDir(subFilePath)
    if (isDir) {
      await recursionfindTarballs(subFilePath, tarballs)
    }
    if (path.extname(subFile) === `.tgz`) {
      tarballs.push(subFilePath)
    }
  }
}

const parseUrl = (url, withEndSlash) => {
  const slash = '/'
  const lastIndex = url.length - 1
  const lastChar = url.substring(lastIndex)
  if (lastChar === slash) {
    return withEndSlash ? url : url.substring(0, lastIndex)
  } else {
    return withEndSlash ? url + slash : url
  }
}

export default class NpmPackagesTruck {
  constructor() {}

  /**
   * 下载
   * @param {{packageLockPath:string, repositoryDir:string}} args
   * @param {(log:string) => void} logCallback
   * @param {(process:{totalCount: number, doneCount: number}) => void} progressCallback
   * @returns
   */
  packing(args, logCallback, progressCallback, options) {
    return downloadFromPackageLock(args, logCallback, progressCallback, options)
  }

  /**
   * 上传
   * @param {{npmHome:string, repositoryUrl:string, username: string, password:string, respositroyDir:string}} args
   * @param {(log:string) => void} logCallback
   * @param {(process:{totalCount: number, doneCount: number}) => void} progressCallback
   */
  async unpacking(args, logCallback, progressCallback, options) {
    const { npmHome, repositoryUrl, username, password, respositroyDir } = args
    const { retry, concurrency } = { ...uploadOptions, ...options }

    const userconfig = path.join(cfg.getAppHome(), 'npmrc')
    const scope = '@packages-truck'

    logCallback("搜索包中...\n")
    let tarballs = []
    await recursionfindTarballs(respositroyDir, tarballs)
    logCallback(`搜索完毕, 共计${tarballs.length}\n`)

    const progress = { totalCount: tarballs.length, doneCount: 0 }
    progressCallback(progress)

    const loginResult = await npm.login({
      registry: parseUrl(repositoryUrl, false),
      scope,
      username,
      password,
      userconfig
    })
    if (!loginResult.isSuccess) {
      logCallback(`[error] login error ${loginResult.msg}`)
    }

    const pLimit = plimit(concurrency)

    const publishPromises = tarballs.map((filePath) => {
      let retryNum = 0
      const uploadPackage = async (packagePath) => {
        retryNum++;
        const { isSuccess, logText, duration } = await npm.publish({
          npmHome,
          registry: parseUrl(repositoryUrl, true),
          userconfig,
          filePath: packagePath
        })

        if (isSuccess) {
          progress.doneCount++
          logCallback(
            `[upload success] (${progress.doneCount}/${progress.totalCount}) ${filePath} ${duration}ms [${retryNum}]\n`
          )
        } else {
          if (retryNum < retry) {
            return uploadPackage(packagePath)
          }
          progress.doneCount++
          logCallback(
            `[upload error] (${progress.doneCount}/${progress.totalCount}) ${filePath} [${retryNum}] \n ${logText}\n`
          )
        }

        progressCallback(progress)

        return {
          isSuccess,
          logText,
          packageInfo: getTarballInfoByPath(respositroyDir, packagePath)
        }
      }

      return pLimit(async () => {
          return uploadPackage(filePath)
      })
    })

    logCallback(`准备上传  重试次数: ${retry}; 并发数: ${concurrency}\n`)
    return Promise.all(publishPromises)
      .then((values) => {
        futils.removeFile(userconfig)
        const errorList = values.filter((item) => !item.isSuccess)
        return {
          totalCount: progress.totalCount,
          errorCount: errorList.length,
          errorList
        }
      })
      .catch((err) => {
        logCallback(err)
      })
  }
}
